﻿using System;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;

namespace Transmute.Graphics
{
    public static class GraphicsHelper
    {
        #region Properties

        private static RenderTarget2D renderTarget;

        public static GraphicsDevice GraphicsDevice;
        public static SpriteBatch SpriteBatch;
        public static Texture2D WhitePixel;

        public static bool IsInitialized { get; private set; }

        #endregion

        #region Initialization

        /// <summary>
        /// Initializes the GraphicsHelper
        /// </summary>
        public static void Initialize(GraphicsDevice device)
        {
            GraphicsDevice = device;
            SpriteBatch = new SpriteBatch(GraphicsDevice);
            WhitePixel = new Texture2D(GraphicsDevice, 1, 1);
            WhitePixel.SetData<Color>(new Color[1] { Color.White });
            IsInitialized = true;
        }

        /// <summary>
        /// Initializes the GraphicsHelper
        /// </summary>
        public static void Initialize(GraphicsDevice device, SpriteBatch spriteBatch)
        {
            GraphicsDevice = device;
            SpriteBatch = spriteBatch;
            WhitePixel = new Texture2D(GraphicsDevice, 1, 1);
            WhitePixel.SetData<Color>(new Color[1] { Color.White });
            IsInitialized = true;
        }

        #endregion

        #region Internal Methods

        private static void CheckInitialization()
        {
            string errorMsg = "";
            if (GraphicsDevice == null) errorMsg += "GraphicsDevice is null.\r\n";
            if (SpriteBatch == null) errorMsg += "SpriteBatch is null.\r\n";
            if (WhitePixel == null) errorMsg += "WhitePixel is null.\r\n";
            if (!string.IsNullOrEmpty(errorMsg))
            {
                throw new Exception(errorMsg + "GraphicsHelper needs initialized.");
            }
        }

        #endregion

        #region Texture Methods

        /// <summary>
        /// Creates a texture of specified size and color
        /// </summary>
        public static Texture2D CreateColorTexture(int width, int height, Color color)
        {
            var texture = new Texture2D(GraphicsDevice, width, height);
            var pixels = new Color[width * height];
            for (var i = 0; i < width * height; i++)
            {
                pixels[i] = color;
            }
            texture.SetData<Color>(pixels);
            return texture;
        }

        /// <summary>
        /// Renders specific action to a texture
        /// </summary>
        public static Texture2D RenderToTexture(int width, int height, Action action)
        {
            CheckInitialization();
            renderTarget = new RenderTarget2D(GraphicsDevice, width, height);

            GraphicsDevice.SetRenderTarget(renderTarget);

            GraphicsDevice.Clear(Color.Transparent);

            SpriteBatch.Begin();
                action();
            SpriteBatch.End();

            GraphicsDevice.SetRenderTarget(null);

            return renderTarget;
        }

        #endregion

        #region Utility Methods

        /// <summary>
        /// Determines distance between two points
        /// </summary>
        public static double GetDistance(float sx, float sy, float ex, float ey)
        {
            var a = (ex - sx);
            var b = (ey - sy);
            return Math.Sqrt((a * a) + (b * b));
        }

        /// <summary>
        /// Determines distance between two points
        /// </summary>
        public static double GetDistance(Vector2 start, Vector2 end)
        {
            return GetDistance(start.X, start.Y, end.X, end.Y);
        }

        /// <summary>
        /// Determines distance between two points
        /// </summary>
        public static double GetDistance(int sx, int sy, int ex, int ey)
        {
            return GetDistance((float)sx, (float)sy, (float)ex, (float)ey);
        }

        #endregion

        #region Line Methods

        /// <summary>
        /// Draws a line from Point A (sx, sy), to Point B (ex, ey) with a specified thickness and color
        /// </summary>
        public static void DrawLine(float sx, float sy, float ex, float ey, int lineThickness, Color color)
        {
            CheckInitialization();
            var d = GetDistance(sx, sy, ex, ey);
            var r = Math.Atan2(sy - ey, sx - ex) + (Math.PI * 2) * 0.25f;
            var drawRect = new Rectangle((int)sx, (int)sy, (int)lineThickness, (int)d);
            SpriteBatch.Draw(WhitePixel, drawRect, null, color, (float)r, Vector2.Zero, SpriteEffects.None, 0f);
        }

        /// <summary>
        /// Draws a line from Point A (start), to Point B (end) with a specified thickness and color
        /// </summary>
        public static void DrawLine(Vector2 start, Vector2 end, int lineThickness, Color color)
        {
            DrawLine(start.X, start.Y, end.X, end.Y, lineThickness, color);
        }

        /// <summary>
        /// Draws a line from Point A (sx, sy), to Point B (ex, ey) with a specified thickness and color
        /// </summary>
        public static void DrawLine(int sx, int sy, int ex, int ey, int lineThickness, Color color)
        {
            DrawLine((float)sx, (float)sy, (float)ex, (float)ey, lineThickness, color);
        }

        #endregion

        #region Rectangle Methods

        /// <summary>
        /// Draws a rectangle
        /// </summary>
        public static void DrawRectangle(int x, int y, int width, int height, int lineThickness, Color borderColor)
        {
            DrawLine(x, y, x + width, y, lineThickness, borderColor);
            DrawLine(x, y, x, y + height, lineThickness, borderColor);
            DrawLine(x, y + height, x + width, y + height, lineThickness, borderColor);
            DrawLine(x + width, y - lineThickness, x + width, y + height, lineThickness, borderColor);
        }

        /// <summary>
        /// Draws a filled rectangle
        /// </summary>
        public static void DrawFilledRectangle(int x, int y, int width, int height, int lineThickness, Color borderColor, Color fillColor)
        {
            CheckInitialization();
            SpriteBatch.Draw(WhitePixel, new Rectangle(x, y, width, height), fillColor);
            DrawRectangle(x, y, width, height, lineThickness, borderColor);
        }

        /// <summary>
        /// Draws a filled rectangle
        /// </summary>
        public static void DrawFilledRectangle(int x, int y, int width, int height, int lineThickness, Color fillColor)
        {
            DrawFilledRectangle(x, y, width, height, lineThickness, fillColor, fillColor);
        }

        /// <summary>
        /// Draws a rectangle
        /// </summary>
        public static void DrawRectangle(Rectangle rect, int lineThickness, Color borderColor)
        {
            DrawRectangle(rect.X, rect.Y, rect.Width, rect.Height, lineThickness, borderColor);
        }

        /// <summary>
        /// Draws a filled rectangle
        /// </summary>
        public static void DrawFilledRectangle(Rectangle rect, int lineThickness, Color borderColor, Color fillColor)
        {
            DrawFilledRectangle(rect.X, rect.Y, rect.Width, rect.Height, lineThickness, borderColor, fillColor);
        }

        /// <summary>
        /// Draws a filled rectangle
        /// </summary>
        public static void DrawFilledRectangle(Rectangle rect, int lineThickness, Color fillColor)
        {
            DrawFilledRectangle(rect.X, rect.Y, rect.Width, rect.Height, lineThickness, fillColor);
        }

        #endregion

        #region Pixel Methods

        /// <summary>
        /// Draws a pixel
        /// </summary>
        public static void DrawPixel(int x, int y, int pixelSize, Color color)
        {
            CheckInitialization();
            SpriteBatch.Draw(WhitePixel, new Rectangle(x, y, pixelSize, pixelSize), color);
        }

        /// <summary>
        /// Draws a pixel
        /// </summary>
        public static void DrawPixel(int x, int y, Color color)
        {
            DrawPixel(x, y, 1, color);
        }

        /// <summary>
        /// Draws a pixel
        /// </summary>
        public static void DrawPixel(float x, float y, int pixelSize, Color color)
        {
            DrawPixel((int)x, (int)y, pixelSize, color);
        }

        /// <summary>
        /// Draws a pixel
        /// </summary>
        public static void DrawPixel(float x, float y, Color color)
        {
            DrawPixel((int)x, (int)y, color);
        }

        /// <summary>
        /// Draws a pixel
        /// </summary>
        public static void DrawPixel(Vector2 point, int pixelSize, Color color)
        {
            DrawPixel(point.X, point.Y, pixelSize, color);
        }

        /// <summary>
        /// Draws a pixel
        /// </summary>
        public static void DrawPixel(Vector2 point, Color color)
        {
            DrawPixel(point.X, point.Y, color);
        }

        #endregion

        #region Circle Methods

        /// <summary>
        /// Draws a circle
        /// </summary>
        public static void DrawCircle(int cx, int cy, float radius, Color borderColor)
        {
            DrawEllipse(cx, cy, radius, radius, 1, borderColor);
        }

        /// <summary>
        /// Draws a circle
        /// </summary>
        public static void DrawCircle(int cx, int cy, float radius, int borderThickness, Color borderColor)
        {
            DrawEllipse(cx, cy, radius, radius, borderThickness, borderColor);
        }

        /// <summary>
        /// Draws a filled circle
        /// </summary>
        public static void DrawFilledCircle(int cx, int cy, float radius, Color borderColor, Color fillColor)
        {
            DrawFilledEllipse(cx, cy, radius, radius, 1, borderColor, fillColor);
        }

        /// <summary>
        /// Draws a filled circle
        /// </summary>
        public static void DrawFilledCircle(int cx, int cy, float radius, int borderThickness, Color borderColor, Color fillColor)
        {
            DrawFilledEllipse(cx, cy, radius, radius, borderThickness, borderColor, fillColor);
        }

        #endregion

        #region Ellipse Methods

        /// <summary>
        /// Draws an Ellipse
        /// </summary>
        public static void DrawEllipse(int xc, int yc, float width, float height, int borderSize, Color borderColor)
        {
            CheckInitialization();

            var a = width;
            var b = height;
            var x = 0;
            var y = b;
            var a2 = a * a;
            var b2 = b * b;
            var crit1 = -(a2 / 4 + a % 2 + b2);
            var crit2 = -(b2 / 4 + b % 2 + a2);
            var crit3 = -(b2 / 4 + b % 2);
            var t = -a2 * y;
            var dxt = 2 * b2 * x;
            var dyt = -2 * a2 * y;
            var d2xt = 2 * b2;
            var d2yt = 2 * a2;

            while (y >= 0 && x <= a)
            {
                DrawPixel(xc + x, yc + y, borderSize, borderColor);

                if (x != 0 || y != 0) DrawPixel(xc - x, yc - y, borderSize, borderColor);

                if (x != 0 && y != 0)
                {
                    DrawPixel(xc + x, yc - y, borderSize, borderColor);
                    DrawPixel(xc - x, yc + y, borderSize, borderColor);
                }

                if (t + b2 * x <= crit1 || t + a2 * y <= crit3)
                {
                    x++;
                    dxt += d2xt;
                    t += dxt;
                }
                else if (t - a2 * y > crit2)
                {
                    y--;
                    dyt += d2yt;
                    t += dyt;
                }
                else
                {
                    x++;
                    dxt += d2xt;
                    t += dxt;

                    y--;
                    dyt += d2yt;
                    t += dyt;
                }
            }
        }

        /// <summary>
        /// Draws an Ellipse
        /// </summary>
        public static void DrawFilledEllipse(int xc, int yc, float w, float h, int borderThickness, Color borderColor, Color fillColor)
        {
            CheckInitialization();

            var ox = xc;
            var oy = yc;
            var owidth = w;
            var oheight = h;
            var a = w;
            var b = h;
            var width = 1;
            var height = 1;
            var x = 0;
            var y = b;
            var rx = x;
            var ry = y;
            var a2 = a * a;
            var b2 = b * b;
            var crit1 = -(a2 / 4 + a % 2 + b2);
            var crit2 = -(b2 / 4 + b % 2 + a2);
            var crit3 = -(b2 / 4 + b % 2);
            var t = -a2 * y;
            var dxt = 2 * b2 * x;
            var dyt = -2 * a2 * y;
            var d2xt = 2 * b2;
            var d2yt = 2 * a2;

            if (b == 0)
            {
                DrawFilledRectangle((int)(xc - a), (int)yc, (int)(2 * a + 1), 1, 1, borderColor, fillColor);
                return;
            }

            while (y >= 0 && x <= a)
            {
                if (t + b2 * x <= crit1 || t + a2 * y <= crit3)
                {
                    if (height == 1) { }
                    else if (ry * 2 + 1 > (height - 1) * 2)
                    {
                        DrawFilledRectangle((int)(xc - rx), (int)(yc - ry), width, height - 1, 1, fillColor);
                        DrawFilledRectangle((int)(xc - rx), (int)(yc + ry + 1), width, 1 - height, 1, fillColor);
                        ry -= height - 1;
                        height = 1;
                    }
                    else
                    {
                        DrawFilledRectangle((int)(xc - rx), (int)(yc - ry), width, (int)(ry * 2 + 1), 1, fillColor);
                        ry -= ry;
                        height = 1;
                    }
                    x++;
                    dxt += d2xt;
                    t += dxt;
                    rx++;
                    width += 2;
                }
                else if (t - a2 * y > crit2)
                {
                    y--;
                    dyt += d2yt;
                    t += dyt;
                    height++;
                }
                else
                {
                    if (ry * 2 + 1 > height * 2)
                    {
                        DrawFilledRectangle((int)(xc - rx), (int)(yc - ry), width, height, 1, fillColor);
                        DrawFilledRectangle((int)(xc - rx), (int)(yc + ry + 1), width, -height, 1, fillColor);
                    }
                    else
                    {
                        DrawFilledRectangle((int)(xc - rx), (int)(yc - ry), width, (int)(ry * 2 + 1), 1, fillColor);
                    }
                    x++;
                    dxt += d2xt;
                    t += dxt;
                    y--;
                    dyt += d2yt;
                    t += dyt;
                    rx++;
                    width += 2;
                    ry -= height;
                    height = 1;
                }

                if (ry > height)
                {
                    DrawFilledRectangle((int)(xc - rx), (int)(yc - ry), width, height, 1, fillColor);
                    DrawFilledRectangle((int)(xc - rx), (int)(yc + ry + 1), width, -height, 1, fillColor);
                }
                else
                {
                    DrawFilledRectangle((int)(xc - rx), (int)(yc - ry), width, (int)(ry * 2 + 1), 1, fillColor);
                }
            }

            DrawEllipse(ox, oy, owidth + borderThickness, oheight + borderThickness, borderThickness, borderColor);
        }

        /// <summary>
        /// Draws an Ellipse
        /// </summary>
        public static void DrawFilledEllipse(int xc, int yc, float w, float h, int borderThickness, Color fillColor)
        {
            DrawFilledEllipse(xc, yc, w, h, borderThickness, fillColor, fillColor);
        }

        #endregion

        #region Triangle Methods

        /// <summary>
        /// Draws a triangle
        /// </summary>
        public static void DrawTriangle(int x1, int y1, int x2, int y2, int x3, int y3, int borderThickness, Color borderColor)
        {
            DrawLine(x1, y1, x2, y2, borderThickness, borderColor);
            DrawLine(x2, y2, x3, y3, borderThickness, borderColor);
            DrawLine(x3, y3, x1, y1, borderThickness, borderColor);
        }

        /// <summary>
        /// Draws a triangle
        /// </summary>
        public static void DrawTriangle(Vector2 pointA, Vector2 pointB, Vector2 pointC, int borderThickness, Color borderColor)
        {
            DrawTriangle((int)pointA.X, (int)pointA.Y, (int)pointB.X, (int)pointB.Y, (int)pointC.X, (int)pointC.Y, borderThickness, borderColor);
        }

        #endregion
                
    }
}
